﻿// Copyright (C) 2009 Choonghwan Lee (linjus.lee@gmail.com)
//
// This file is part of amanda.
//
// amanda is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// amanda is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with amanda. If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Drawing;
using System.IO;
using System.ComponentModel;
using System.Drawing.Imaging;
using System.Diagnostics;

namespace manager
{
	static class EncodingExtension
	{
		public static string Extension(Encoding type)
		{
			switch (type) {
				case Encoding.MP3:
					return "mp3";
				case Encoding.FLAC:
					return "flac";
				case Encoding.OGG:
					return "ogg";
			}
			throw new Exception("unimplemented");
		}

		public static string[] Extensions
		{
			get
			{
				return new string[]
				{
					"mp3",
					"flac",
					"ogg",
				};
			}
		}
	}

	public enum SynchronizedStatus
	{
		JUNK,
		NOT_YET,
		UP_TO_DATE,
	}

	public abstract class RuleAction
	{
		protected string TmpDirectory
		{
			get
			{
				return Configuration.Config.TempDirectory;
			}
		}

		public abstract string IconPath { get; }
	}

	public interface IRenameAction
	{
		void Rename(Track track, string[] input, out string[] output);
	}

	public class ClearAction : RuleAction
	{
		public void Commit()
		{
			if (Directory.Exists(this.TmpDirectory))
				Directory.Delete(this.TmpDirectory, true);
			Directory.CreateDirectory(this.TmpDirectory);
		}

		public override string IconPath
		{
			get { return null; }
		}
	}

	public class CopyAction : RuleAction
	{
		public void Commit(string audiopath, out string copiedpath)
		{
			copiedpath = this.TmpDirectory + "encoded";

			int extbegins = audiopath.LastIndexOf('.');
			if (extbegins > -1)
				copiedpath += audiopath.Substring(extbegins);

			File.Copy(audiopath, copiedpath);
		}

		public override string IconPath
		{
			get { return @"Resources\action_copy.png"; }
		}
	}

	public class TranscodingAction : RuleAction, IRenameAction
	{
		private Encoding sourceType;

		public TranscodingAction(Encoding sourceType)
		{
			this.sourceType = sourceType;
		}

		private void Execute(string program, string arguments)
		{
			System.Diagnostics.ProcessStartInfo si = new System.Diagnostics.ProcessStartInfo(program, arguments);
			si.CreateNoWindow = true;
			si.RedirectStandardInput = false;
			si.RedirectStandardOutput = false;
			si.RedirectStandardError = false;
			si.WindowStyle = ProcessWindowStyle.Hidden;

			System.Diagnostics.Process proc = System.Diagnostics.Process.Start(si);
			proc.WaitForExit();
			if (proc.ExitCode != 0)
				throw new Exception("transcoding error " + proc.ExitCode);
		}

		private void Execute(string template, string input, string output)
		{
			template = template.Replace("%input%", "\"" + input + "\"");
			template = template.Replace("%output%", "\"" + output + "\"");

			string[] cmd = template.Split(new char[] { ' ' }, 2);
			this.Execute(cmd[0], cmd[1]);
		}

		private string ReplaceExtension(string input, string extension)
		{
			int extensionbegins = input.LastIndexOf('.');
			if (extensionbegins == -1)
				return input + '.' + extension;
			return input.Substring(0, extensionbegins) + '.' + extension;
		}

		private void TranscodeOgg(string audiopath, Encoding encoding, out string outputpath)
		{
			string wavepath = this.ReplaceExtension(audiopath, "wav");
			outputpath = this.ReplaceExtension(audiopath, EncodingExtension.Extension(encoding));

			this.Execute(Configuration.Config.OGG_to_WAVE, audiopath, wavepath);
			this.Execute(Configuration.Config.WAVE_to_MP3, wavepath, outputpath);
		}

		private void TranscodeFlac(string audiopath, Encoding encoding, out string outputpath)
		{
			string interpath = this.ReplaceExtension(audiopath, "wav");
			outputpath = this.ReplaceExtension(audiopath, EncodingExtension.Extension(encoding));

			this.Execute(Configuration.Config.FLAC_to_WAVE, audiopath, interpath);
			this.Execute(Configuration.Config.WAVE_to_MP3, interpath, outputpath);
		}

		public void Commit(string audiopath, DeviceInfo device, Track track, out string outputpath)
		{
			outputpath = null;

			switch (this.sourceType) {
				case Encoding.FLAC:
					System.Diagnostics.Debug.Assert(device.TranscodedType(this.sourceType) == Encoding.MP3);
					this.TranscodeFlac(audiopath, Encoding.MP3, out outputpath);
					break;
				case Encoding.OGG:
					System.Diagnostics.Debug.Assert(device.TranscodedType(this.sourceType) == Encoding.MP3);
					this.TranscodeOgg(audiopath, Encoding.MP3, out outputpath);
					break;
				case Encoding.MP3:
					throw new Exception("mp3?");
				case Encoding.ETC:
					throw new Exception("unsupported type");
			}

			if (outputpath != null)
				this.PutTags(track, outputpath);
		}

		private void PutTags(Track track, string path)
		{
			TagLib.File file = TagLib.File.Create(path);
			TagLib.Id3v2.Tag tag2 = (TagLib.Id3v2.Tag)file.GetTag(TagLib.TagTypes.Id3v2);

			tag2.Album = track.Album;
			tag2.Performers = new string[] { track.Artist };
			tag2.Title = track.TrackName;

			file.Save();
		}

		public override string IconPath
		{
			get { return @"Resources\action_transcode.png"; }
		}

		#region IRenameAction Members

		public void Rename(Track track, string[] input, out string[] output)
		{
			output = new string[input.Length];
			for (int i = 0; i < input.Length; ++i) {
				if (i == input.Length - 1)
					output[i] = this.ReplaceExtension(input[i], EncodingExtension.Extension(Encoding.MP3));
				else
					output[i] = input[i];
			}
		}

		#endregion
	}

	public class RomanizationAction : RuleAction, IRenameAction
	{
		public void Commit(string audiopath, Track track)
		{
			TagLib.File file = TagLib.File.Create(audiopath);
			TagLib.Id3v2.Tag tag2 = (TagLib.Id3v2.Tag)file.GetTag(TagLib.TagTypes.Id3v2);

			if (tag2.Album != null)
				tag2.Album = Romanization.Convert(tag2.Album);
			if (tag2.Title != null)
				tag2.Title = Romanization.Convert(tag2.Title);
			tag2.AlbumArtists = this.Convert(tag2.AlbumArtists);
			tag2.Performers = this.Convert(tag2.Performers);
			tag2.Artists = this.Convert(tag2.Artists);

			file.Save();
		}

		private string[] Convert(string[] input)
		{
			string[] ret = new string[input.Length];
			for (int i = 0; i < input.Length; ++i)
				ret[i] = Romanization.Convert(input[i]);
			return ret;
		}

		public override string IconPath
		{
			get { return @"Resources\action_romanitization.png"; }
		}

		#region IRenameAction Members

		public void Rename(Track track, string[] input, out string[] output)
		{
			output = new string[input.Length];
			for (int i = 0; i < input.Length; ++i)
				output[i] = Romanization.Convert(input[i]);
		}

		#endregion
	}

	public class ArtworkEmbedAction : RuleAction
	{
		public void Commit(string audiopath, string sourcepath)
		{
			int folderstarts = sourcepath.LastIndexOf('\\');
			if (folderstarts == -1) return;
			string basepath = sourcepath.Substring(0, folderstarts + 1);
			string picturepath = basepath + "folder.jpg";
			if (!File.Exists(picturepath)) return;

			this.EmbedArtwork(audiopath, picturepath);
		}

		// Palm Pre
		// I have tried many things, but I concluded that TextEncoding really matters.
		private void EmbedArtwork(string audiopath, string picturepath)
		{
			picturepath = this.ResizeImage(picturepath);

			TagLib.Picture front = new TagLib.Picture(picturepath);
			front.Type = TagLib.PictureType.FrontCover;
			front.Description = "";

			// TODO: one bad side-effect is that Id3v1 tag is added.
			// I think this library has a bug that does not distinguish v1 from v2.
			TagLib.File file = TagLib.File.Create(audiopath);
			TagLib.Id3v2.Tag tag2 = (TagLib.Id3v2.Tag)file.GetTag(TagLib.TagTypes.Id3v2);

			TagLib.Id3v2.AttachedPictureFrame frame = new TagLib.Id3v2.AttachedPictureFrame(front);
			frame.TextEncoding = TagLib.StringType.Latin1;
			tag2.Pictures = new TagLib.Id3v2.AttachedPictureFrame[] { frame };

			// When I called RemoveTags(v1), I noticed that it removes not only v1 but also v2.
//			file.RemoveTags(TagLib.TagTypes.Id3v1);

			file.Save();
		}

		private string ResizeImage(string picturepath)
		{
			int threshold = 300;
			using (Image image = Image.FromFile(picturepath)) {
				if (image.Width > threshold || image.Height > threshold) {
					int width, height;
					if (image.Width < image.Height) {
						height = threshold;
						width = (int)((image.Width * threshold) / (float)image.Height);
					}
					else {
						width = threshold;
						height = (int)((image.Height * threshold) / (float)image.Width);
					}

					using (Bitmap bm = new Bitmap(threshold, threshold)) {
						using (Graphics g = Graphics.FromImage(bm)) {
							g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
							g.DrawImage(image, 0, 0, threshold, threshold);

							string resizedpath = this.TmpDirectory + "resized.jpg";
							ImageCodecInfo codec = null;
							foreach (ImageCodecInfo c in ImageCodecInfo.GetImageEncoders()) {
								if (c.MimeType == "image/jpeg") {
									codec = c;
									break;
								}
							}
							if (codec == null)
								throw new Exception("image/jpeg encoder does not exit");
							EncoderParameters encparam = new EncoderParameters(1);
							encparam.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
							bm.Save(resizedpath, codec, encparam);

							picturepath = resizedpath;
						}
					}
				}
			}
			return picturepath;
		}

		public override string IconPath
		{
			get { return @"Resources\action_embed_artwork.png"; }
		}
	}

	public class FlattenStructureAction : RuleAction, IRenameAction
	{
		private int level;

		public FlattenStructureAction()
		{
			this.level = 1;
		}

		public override string IconPath
		{
			get { return null; }
		}

		#region IRenameAction Members

		public void Rename(Track track, string[] input, out string[] output)
		{
			if (input.Length <= 2) {
				output = new string[level + 1];
				for (int i = 0; i < level + 1; ++i)
					output[i] = input[i];
			}
			else {
				output = new string[level + 1];
				for (int i = 0; i < level + 1; ++i)
					output[i] = input[input.Length - (level + 1) + i];
			}
		}

		#endregion
	}

	public class Track : INotifyPropertyChanged
	{
		private string artist;
		private string album;
		private string trackname;
		private Encoding encoding;
		private string sourcepath;
		private string devicepath;
		private SynchronizedStatus syncstatus;
		private bool forcesync;
		private List<RuleAction> actions;

		public string Artist
		{ get { return this.artist; } }

		public string Album
		{ get { return this.album; } }

		public string TrackName
		{ get { return this.trackname; } }

		public Encoding Encoding
		{ get { return this.encoding; } }

		public string Path
		{ get { return this.sourcepath; } }

		public string DevicePath
		{
			get { return this.devicepath; }
			set
			{
				this.devicepath = value;
			}
		}

		public SynchronizedStatus SyncStatus
		{
			get { return this.syncstatus; }
			set
			{
				this.syncstatus = value;
				NotifyPropertyChanged("SyncStatusIconPath");
			}
		}

		public bool ForceSync
		{
			get { return this.forcesync; }
			set
			{
				this.forcesync = value;
				NotifyPropertyChanged("SyncStatusIconPath");
			}
		}

		public string SyncStatusIconPath
		{
			get
			{
				if (this.forcesync) {
					return @"Resources\sync_force.png";
				}
				else {
					switch (this.syncstatus) {
						case SynchronizedStatus.JUNK:
							return @"Resources\sync_unknown.png";
						case SynchronizedStatus.NOT_YET:
							return @"Resources\sync_notyet.png";
						case SynchronizedStatus.UP_TO_DATE:
							return @"Resources\sync_uptodate.png";
					}
				}
				return null;
			}
		}

		public List<RuleAction> Actions
		{ get { return this.actions; } }

		public string Action1IconPath
		{ get { return this.ActionIcon(0); } }
		public string Action2IconPath
		{ get { return this.ActionIcon(1); } }
		public string Action3IconPath
		{ get { return this.ActionIcon(2); } }
		public string Action4IconPath
		{ get { return this.ActionIcon(3); } }
		public string Action5IconPath
		{ get { return this.ActionIcon(4); } }

		private string ActionIcon(int index)
		{
			if (index < this.actions.Count)
				return this.actions[index].IconPath;
			return null;
		}

		internal Track(string devpath)
		{
			this.encoding = Encoding.ETC;
			this.syncstatus = SynchronizedStatus.JUNK;
			this.devicepath = devpath;
		}

		internal Track(string artist, string album, string trackname, Encoding encoding, string path, SynchronizedStatus status, DeviceInfo device)
		{
			this.artist = artist;
			this.album = album;
			this.trackname = trackname;
			this.encoding = encoding;
			this.sourcepath = path;
			this.devicepath = null;
			this.syncstatus = status;
			this.actions = device.DetermineActions(this);
			if (this.syncstatus != SynchronizedStatus.UP_TO_DATE)
				this.actions.Insert(0, new CopyAction());
		}

		#region INotifyPropertyChanged Members

		public event PropertyChangedEventHandler PropertyChanged;

		#endregion

		private void NotifyPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}
	}

	public class Tracks : ObservableCollection<Track>
	{
	}
}
